﻿namespace MonetDbDataProvider.Mapi
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net.Sockets;
    using System.Text;

    using MonetDbDataProvider.Query;
    using MonetDbDataProvider.Security;
    using MonetDbDataProvider.Templates;

    public class MapiSocket : IDisposable
    {
        private readonly ICryptoProvider cryptoProvider;        
        
        private TcpClient tcpClient;

        private int timeout;

        private int receiveBuffer;

        private int sendBuffer;

        private string language;

        private StreamReader streamReader;

        private StreamWriter streamWriter;

        public MapiSocket(ICryptoProvider cryptoProvider)
        {
            this.cryptoProvider = cryptoProvider;
        }

        public string Language
        {
            get
            {
                if (string.IsNullOrEmpty(this.language)) this.language = "sql";

                return this.language;
            } 

            set
            {
                this.language = value;
            }
        }

        public int ProtocolVersion { get; set; }

        public string Database { get; set; }

        public int TimeOut
        {
            get { return this.timeout == 0 ? (60 * 2 * 1000) : this.timeout; }

            set { this.timeout = value; }
        }

        public int ReceiveBuffer 
        { 
            get { return this.receiveBuffer == 0 ? (60 * 2 * 1000) : this.receiveBuffer; }

            set { this.receiveBuffer = value; } 
        }

        public int SendBuffer
        {
            get { return this.sendBuffer == 0 ? (60 * 2 * 1000) : this.sendBuffer; }

            set { this.sendBuffer = value; }
        }

        public List<string> Connect(ConnectionString connectionString)
        {
            var errors = new List<string>();
            var warnings = new List<string>();
            var redirects = new List<string>();

            this.tcpClient = new TcpClient(connectionString.Host, connectionString.Port)
                                 {
                                     NoDelay = true,
                                     ReceiveTimeout = this.TimeOut,
                                     ReceiveBufferSize = this.ReceiveBuffer,
                                     SendBufferSize = this.SendBuffer
                                 };

            this.streamReader = new StreamReader(new MonetDbBlockStream(this.tcpClient.GetStream()));
            this.streamWriter = new StreamWriter(new MonetDbBlockStream(this.tcpClient.GetStream()));

            var challenge = this.streamReader.ReadLine();
            var prompt = this.streamReader.ReadLine();
            var hash = this.GetChallengeReponse(challenge, connectionString.Username, connectionString.Password, connectionString.Database);

            this.streamWriter.WriteLine(hash);
            this.streamWriter.Flush();

            var tmp = this.streamReader.ReadLine();

            while (tmp.Substring(0, 1) != LineTypes.Prompt)
            {
                if (string.IsNullOrEmpty(tmp))
                    ExceptionGenerator.Generate("Lost connection to server.");

                switch (tmp.Substring(0, 1))
                {
                    case LineTypes.Error:
                        errors.Add(tmp.Substring(1));
                        break;
                    case LineTypes.Info:
                        warnings.Add(tmp.Substring(1));
                        break;
                    case LineTypes.Redirect:
                        redirects.Add(tmp.Substring(1));
                        break;
                }

                tmp = this.streamReader.ReadLine();
            }

            if (errors.Any())
                ExceptionGenerator.Generate(string.Concat(errors.Select(e => e + ", ")));

            if (redirects.Any())
            {
                this.tcpClient.Client.Close();
                this.tcpClient.Close();

                return this.FollowRedirect(redirects, connectionString.Username, connectionString.Password);
            }

            return warnings;
        }

        public IList<QueryResult> ExecuteCommand(string sql)
        {
            this.streamWriter.WriteLine(QueryTemplate.Query(sql));
            this.streamWriter.Flush();

            IList<string> response = new List<string>();

            var tmp = this.streamReader.ReadLine();

            while (tmp != LineTypes.Prompt)
            {
                response.Add(tmp);
                tmp = this.streamReader.ReadLine();
            }

            CheckErrorResponse(response);

            return ParseBatchResponse(response).Select(batch => new QueryResult(batch)).ToList();
        }

        public void Close()
        {
            this.Dispose();
        }

        public void Dispose()
        {
            if (this.tcpClient.Connected) 
                this.tcpClient.Close();
        }

        private List<string> FollowRedirect(IList<string> redirects, string username, string password)
        {
            var redirectUri = new Uri(redirects[0]);
            var database = string.Empty;
            var query = redirectUri.Query;
            var warnings = new List<string>();

            if (redirectUri.Scheme.Equals("monetdb"))
                throw new IOException("Redirect is to another server. This is type of redirect is not supported at this time.");

            // Query can look like "mapi:monetdb://host:port/database?arg=value&..."
            var queryParams = query.Replace("?", string.Empty).Split('&');
         
            foreach (var param in queryParams.Select(queryParam => queryParam.Split('=')).Where(param => param.Length == 2))
            {
                switch (param[0])
                {
                    case "database":
                        database = param[1];
                        warnings.Add("Redirect points to a different database: " + database);
                        break;
                    case "language":
                        this.Language = param[1];
                        warnings.Add("Redirect specifies use of a different language: " + this.Language);
                        break;
                    default:
                        warnings.Add("Ignoring unknown paramter: " + param[0]);
                        break;
                }
            }

            var connectionString = string.Format(
                "host={0},port={1},username={2},password={3},database={4}",
                redirectUri.Host,
                redirectUri.Port,
                username,
                password,
                database);

            return this.Connect(new ConnectionString(connectionString));
        }

        private static List<List<string>> ParseBatchResponse(IList<string> response)
        {
            var parsedBatch = new List<List<string>>();
            var tmp = new List<string>();

            for (var i = 0; i < response.Count; i++)
            {
                if (response[i].Substring(0, 1) == LineTypes.SoHeader)
                {
                    tmp = new List<string>();
                    var rowCount = int.Parse(response[i].Split(' ')[2]);

                    // Check if we have a non-query response. Otherwise we have a query response.
                    if (rowCount == -1) 
                        tmp.Add(response[i]);
                    else
                    {
                        // The first 5 rows are header info. 
                        for (var j = (0 + i); j < ((i + 5) + rowCount); j++)
                        {
                            tmp.Add(response[j]);
                        }                        
                    }

                    parsedBatch.Add(tmp);
                }
            }

            return parsedBatch;
        }

        /// <summary>
        /// Checks for an error response from server. 
        /// Example: !42S01!CREATE TABLE: name 'testtable' already in use
        /// </summary>
        /// <param name="response"></param>
        private static void CheckErrorResponse(IList<string> response)
        {
            if (response.Count == 0)
                throw new MonetDbException("No response returned from MonetDB server.");

            var line = response[0];

            if (line.StartsWith("!"))
                throw new MonetDbException(line.Split('!')[2]);
        }

        private string GetChallengeReponse(string challenge, string username, string password, string database)
        {
            var challengeResponse = new Utility().ParseChallenge(challenge, username, password);
            var hashedPassword = this.cryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(challengeResponse.Password), "SHA512");

            this.ProtocolVersion = challengeResponse.ProtocolVersion;
            var hashResult = this.cryptoProvider.ComputeHash(
                                                    Encoding.UTF8.GetBytes(hashedPassword.HexHash + challengeResponse.ChallengeHash),
                                                    challengeResponse.Hash.Split(','));

            return string.Format("LIT:{0}:{1}{2}:{3}:{4}:", username, string.Concat("{", hashResult.HashType, "}"), hashResult.HexHash, this.Language, database);
        }
    }
}
